<?php
/*
 * Copyright (c) 2013 Eugene Kuzmenko
 *
 * Permission  is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in  the Software without restriction, including without limitation the rights
 * to  use,  copy,  modify,  merge, publish, distribute, sublicense, and/or sell
 * copies  of  the  Software,  and  to  permit  persons  to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The  above  copyright  notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE  SOFTWARE  IS  PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED,  INCLUDING  BUT  NOT  LIMITED  TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS  FOR  A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS  OR  COPYRIGHT  HOLDERS  BE  LIABLE  FOR  ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 *
 * For more information, see <http://www.aljey.com>.
 */

namespace Al\Template\Lib;

use Al\Core\Lib\Service;

/**
 * Helps to easily build source maps for compiled files.
 *
 * @author Eugene Kuzmenko <mail@aljey.com>
 */
class SourceMap {

  /**
   * @var array $sources A collection of source file names.
   */
  private $sources = array();

  /**
   * @var array $mappings A collection of mappings.
   */
  private $mappings = array();

  /**
   * @var string $file The generated source file.
   */
  private $file = '';

  /**
   * @var string $sourceRoot Web location of the original sources.
   */
  private $sourceRoot = '';

  /**
   * Constructor
   *
   * @param string $file The generated source file.
   * @param string $root [optional] Web location of the original sources.
   */
  public function __construct($file, $root = '') {
    $this->file = ($route = Service::get("config.routes.$file")) ? $route
      ->buildPath($file) : $file;
    $this->sourceRoot = $root;
  }

  /**
   * Add a single mapping from original source line and column to the generated
   * source's line and column for this source map being created.
   *
   * @param string $source A source file name.
   * @param int $originalLine [optional] The line number in the original file.
   * @param int $originalColumn [optional] The column number in the original
   *                            file.
   * @param int $generatedLine [optional] The line number in the generated file.
   * @param int $generatedColumn [optional] The column number in the generated
   *                             file.
   */
  public function addMapping($source, $originalLine = 0, $originalColumn = 0,
                             $generatedLine = 0, $generatedColumn = 0) {
    $src = array_search($source, $this->sources, true);
    if ($src === false) {
      $src = count($this->sources);
      $this->sources[] = $source;
    }
    $this->mappings[] = array(
      'source' => $src,
      'originalLine' => $originalLine,
      'originalColumn' => $originalColumn,
      'generatedLine' => $generatedLine,
      'generatedColumn' => $generatedColumn,
    );
  }

  /**
   * Returns a complete map.
   *
   * @return array Source map
   */
  public function getMap() {
    return array(
      'version' => 3,
      'file' => $this->file,
      'sourceRoot' => $this->sourceRoot,
      'sources' => $this->sources,
      'names' => array(),
      'mappings' => $this->serializeMappings(),
    );
  }

  /**
   * Compares two mapping arrays.
   *
   * @param array $a Mapping
   * @param array $b Mapping
   * @return int
   */
  private function compareByGeneratedPositions($a, $b) {
    $cmp = $a['generatedLine'] - $b['generatedLine'];
    if ($cmp) {
      return $cmp;
    }
    $cmp = $a['generatedColumn'] - $b['generatedColumn'];
    if ($cmp) {
      return $cmp;
    }
    $cmp = $a['originalLine'] - $b['originalLine'];
    if ($cmp) {
      return $cmp;
    }
    $cmp = $a['originalColumn'] - $b['originalColumn'];
    if ($cmp) {
      return $cmp;
    }
    return 0;
  }

  /**
   * Returns a variable-length quantity encoded string.
   *
   * @param $value int
   * @return string
   */
  private function base64VLQencode($value) {
    $encoded = '';
    $match = array('A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L',
      'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a',
      'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p',
      'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4',
      '5', '6', '7', '8', '9', '+', '/');
    $vlq = $value < 0 ? ((-$value) << 1) + 1 : ($value << 1) + 0;
    $VLQ_BASE = 1 << 5;
    do {
      $digit = $vlq & ($VLQ_BASE - 1);
      $vlq = ($vlq >> 5) & 134217727;
      if ($vlq > 0) {
        $digit |= $VLQ_BASE;
      }
      $encoded .= $match[$digit];
    } while ($vlq > 0);
    return $encoded;
  }

  /**
   * Serialize the accumulated mappings in to the stream of base 64 VLQs
   * specified by the source map format.
   *
   * @return string
   */
  private function serializeMappings() {
    $previousGeneratedColumn = 0;
    $previousGeneratedLine = 0;
    $previousOriginalColumn = 0;
    $previousOriginalLine = 0;
    $previousSource = 0;
    $result = '';
    usort($this->mappings, array($this, 'compareByGeneratedPositions'));
    foreach ($this->mappings as $i => $mapping) {
      if ($mapping['generatedLine'] !== $previousGeneratedLine) {
        $previousGeneratedColumn = 0;
        while ($mapping['generatedLine'] !== $previousGeneratedLine) {
          $result .= ';';
          $previousGeneratedLine++;
        }
      } else {
        if ($i > 0) {
          if (!$this->compareByGeneratedPositions($mapping,
              $this->mappings[$i - 1])) continue;
          $result .= ',';
        }
      }
      $result .= $this->base64VLQencode($mapping['generatedColumn'] -
        $previousGeneratedColumn);
      $previousGeneratedColumn = $mapping['generatedColumn'];
      $result .= $this->base64VLQencode($mapping['source'] - $previousSource);
      $previousSource = $mapping['source'];
      $result .= $this->base64VLQencode($mapping['originalLine'] -
        $previousOriginalLine);
      $previousOriginalLine = $mapping['originalLine'];
      $result .= $this->base64VLQencode($mapping['originalColumn'] -
        $previousOriginalColumn);
      $previousOriginalColumn = $mapping['originalColumn'];
    }
    return $result;
  }

  /**
   * Returns a complete map as a json string.
   *
   * @return string Source map
   */
  public function __toString() {
    return \json_encode($this->getMap());
  }

} 